/**
 * Java XSL code library
 *
 * Copyright (C) 2010 Benoit Mercier <info@servicelibre.com> — All rights reserved.
 *
 * This file is part of jxsl.
 *
 * jxsl is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, version 3.
 *
 * jxsl is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with jxsl.  If not, see <http://www.gnu.org/licenses/>.
 */

package com.servicelibre.jxsl.scenario.test.xspec;

import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;

import javax.xml.XMLConstants;
import javax.xml.namespace.QName;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.stream.StreamSource;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import javax.xml.xpath.XPathFactoryConfigurationException;

import net.sf.saxon.lib.NamespaceConstant;

import org.apache.commons.io.FileUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.xml.SimpleNamespaceContext;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

import com.servicelibre.jxsl.dstest.Document;
import com.servicelibre.jxsl.scenario.RunReport;
import com.servicelibre.jxsl.scenario.XslScenario;
import com.servicelibre.jxsl.scenario.test.FailureReport;
import com.servicelibre.jxsl.scenario.test.TestReport;
import com.servicelibre.jxsl.scenario.test.XslTestScenarioRunner;

/**
 * Run XML testdoc => generate Xspec XSL to apply on the fly then apply!
 */
public class XspecTestScenarioRunner implements XslTestScenarioRunner {

	private static final String JXSL_TEST_DOCUMENT_PLACEHOLDER = "file:/jxslTestDocument";
	private static final String JXSL_TEST_HREF_PLACEHOLDER = "href=\"" + JXSL_TEST_DOCUMENT_PLACEHOLDER + "\"";
	private static final String JXSL_TEST_SELECT_PLACEHOLDER = "doc\\('" + JXSL_TEST_DOCUMENT_PLACEHOLDER + "'\\)";

	static Logger logger = LoggerFactory.getLogger(XspecTestScenarioRunner.class);

	static {
		System.setProperty("javax.xml.transform.TransformerFactory", XslScenario.SAXON_TRANSFORMER_FACTORY_FQCN);
		System.setProperty("javax.xml.xpath.XPathFactory:"+NamespaceConstant.OBJECT_MODEL_SAXON, "net.sf.saxon.xpath.XPathFactoryImpl");

	}

	private File outputDir = new File(System.getProperty("java.io.tmpdir"));
	private XslScenario xspecTestsGeneratorScenario;
	private XslScenario xspecResultHtmlConvertorScenario;

	private boolean storeResultsInSubDir = true;
	private boolean resultsSubDirWithTimeStamp = true;
	private XPathExpression successXpath;
	private XPathExpression testFailedCount;
	private XPathExpression testCount;
	private TestReport lastRunReport;

	private DocumentBuilder xmlBuilder;
	private File currentXspecFile;
	private XslScenario currentXspecTestsScenario;

	public XspecTestScenarioRunner(File xspecTestsGeneratorFile) {
		super();
		this.xspecTestsGeneratorScenario = new XslScenario(xspecTestsGeneratorFile);
		init();
	}

	public XspecTestScenarioRunner(XslScenario xspecTestsGeneratorScenario) {
		super();
		this.xspecTestsGeneratorScenario = xspecTestsGeneratorScenario;
		init();
	}

	private void init() {

		XPath xpath = null;
		try {
			xpath = XPathFactory.newInstance(NamespaceConstant.OBJECT_MODEL_SAXON).newXPath();
		} catch (XPathFactoryConfigurationException e1) {
			logger.error("Error while creating XPathFactory",e1);
			return;
		}

		SimpleNamespaceContext namespaceContext = new SimpleNamespaceContext();
		namespaceContext.bindNamespaceUri("x", "http://www.jenitennison.com/xslt/xspec");
		xpath.setNamespaceContext(namespaceContext);

		try {

			successXpath = xpath.compile("count(//x:test[@successful ='false'] ) = 0");
			testFailedCount = xpath.compile("count(//x:test[@successful ='false'] )");
			testCount = xpath.compile("count(//x:test)");
		} catch (XPathExpressionException e) {
			logger.error("Error while initializing {}.", this.getClass().getName(), e);
		}

		DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance();
		docFactory.setNamespaceAware(true);

		try {
			xmlBuilder = docFactory.newDocumentBuilder();
		} catch (ParserConfigurationException e) {
			logger.error("Error while configuring XML parser", e);
		}

	}

	public void reset() {
		xspecTestsGeneratorScenario.getTransformer().reset();
	}

	public void setOutputDir(File outputDir) {
		this.outputDir = outputDir;

	}

	public File getOutputDir() {
		return outputDir;
	}

	public TestReport getLastRunReport() {
		return this.lastRunReport;
	}

	@Override
	public TestReport run(File xspecFile) {
		return run(xspecFile, outputDir, null);
	}

	@Override
	public TestReport run(File xspecFile, File testOutputDir) {
		return run(xspecFile, testOutputDir, null);
	}

	@Override
	public TestReport run(File xspecFile, Document xmlDoc) {
		return run(xspecFile, outputDir, xmlDoc);
	}

	/**
	 * 
	 * TODO add optional Document parameter
	 * 
	 * // TODO improve performance In order to improve performance, we should
	 * not regenerate/recompile xspec xsl. We should instead compile it once and
	 * change/set parameter (document URL) at each execution. This implies that
	 * we have to keep a compiled version of the Xsl for all runs of the
	 * testScenario with the same xspec file.
	 */
	@Override
	public TestReport run(File xspecFile, File outputDir, Document xmlDoc) {

		this.outputDir = outputDir;

		RunReport testRunReport = null;
		TestReport testReport = new TestReport();

		// Generate custom test XSL
		RunReport generationReport = generateTestFile(xspecFile);
		File generatedTestFile = generationReport.mainOutputFile;

		if (generatedTestFile != null && generatedTestFile.exists()) {

			// Execute the xspec test
			testRunReport = executeTest(xspecFile, generatedTestFile, generationReport.outputProperties.getProperty("encoding"), xmlDoc);

			// Produce HTML report if transformation scenario provided
			if (xspecResultHtmlConvertorScenario != null) {
				RunReport htmlrunReport = generateHtmlReport(testRunReport.mainOutputFile);

				if (testRunReport != null) {
					testRunReport.otherOutputFiles.add(htmlrunReport.mainOutputFile);
				}
			}

			if (testRunReport != null)

			{

				testReport.executionTime = testRunReport.executionTime;
				testReport.executionDate = testRunReport.executionDate;

				testReport.success = getSuccess(testRunReport.mainOutputFile);

				if (!testReport.success) {
					testReport.failureReport = getFailureReport(testRunReport.mainOutputFile);
				}

				if (testRunReport.otherOutputFiles.size() > 0) {
					try {
						testReport.reportUrl = testRunReport.otherOutputFiles.get(0).toURI().toURL();
					} catch (MalformedURLException e) {
						logger.error("Error while converting test report File to URL.", e);
					}
				}

				try {

					org.w3c.dom.Document xspecResultDoc = xmlBuilder.parse(testRunReport.mainOutputFile);

					testReport.testCount = ((Double) testCount.evaluate(xspecResultDoc, XPathConstants.NUMBER)).intValue();
					testReport.testFailedCount = ((Double) testFailedCount.evaluate(xspecResultDoc, XPathConstants.NUMBER)).intValue();

				} catch (SAXException e) {
					logger.error("Error while creating failure report", e);
				} catch (IOException e) {
					logger.error("Error while creating failure report", e);
				} catch (XPathExpressionException e) {
					logger.error("Error while evaluating XPath during failure report creation", e);
				}

			} else {
				testReport.success = false;

			}

		} else {
			logger.error("Unable to find Xspec generated test file.");
		}

		return testReport;

	}

	private void addParamToGeneratedTestFile(File generatedTestFile, String encoding) {

		// Ajouter <xsl:param name="docUrl" required="yes"/>
		try {
			String fileString = FileUtils.readFileToString(generatedTestFile, encoding);

			// file:/jxslTestDocument => {$docUrl}
			String newContent = fileString.replaceAll(JXSL_TEST_HREF_PLACEHOLDER, "href=\"\\{\\$docUrl\\}\"");

			newContent = newContent.replaceAll(JXSL_TEST_SELECT_PLACEHOLDER, "doc\\(\\$docUrl\\)");
			newContent = newContent.replace("</xsl:stylesheet>", "<xsl:param name=\"docUrl\" required=\"yes\"/></xsl:stylesheet>");

			String generatedTestFilePath = generatedTestFile.getAbsolutePath();
			File newFile = new File(generatedTestFilePath + ".new");

			FileUtils.writeStringToFile(newFile, newContent, encoding);
			FileUtils.deleteQuietly(generatedTestFile);

			if (!newFile.renameTo(new File(generatedTestFilePath))) {
				logger.error("Error while renaming {} to {}", newFile, generatedTestFilePath);
			}

		} catch (IOException e) {
			logger.error("Error while replacing jxslTestDocument placeholder", e);
		}

	}

	private RunReport generateHtmlReport(File xmlResultFile) {

		RunReport runReport = new RunReport();

		if (xspecResultHtmlConvertorScenario != null) {
			xspecResultHtmlConvertorScenario.getTransformer().reset();
			xspecResultHtmlConvertorScenario.setSaveOutputOnDisk(true);
			xspecResultHtmlConvertorScenario.setMainOutputDir(xmlResultFile.getParentFile());
			xspecResultHtmlConvertorScenario.setStoreResultsInSubDir(false);
			xspecResultHtmlConvertorScenario.setName("htmlConvertor");
			xspecResultHtmlConvertorScenario.setMainOutputName(xmlResultFile.getName().replace(".xml", ".html"));
			xspecResultHtmlConvertorScenario.apply(xmlResultFile);
			runReport = xspecResultHtmlConvertorScenario.getLastRunReport();

		}
		return runReport;
	}

	/**
	 * Execute xspec test
	 * 
	 * @param xspecFile
	 * @param generatedTestFile
	 * @param generatedTestFileEncoding
	 * @param xmlDoc
	 * @return
	 */
	private RunReport executeTest(File xspecFile, File generatedTestFile, String generatedTestFileEncoding, Document xmlDoc) {

		if (currentXspecFile == null || !currentXspecFile.getAbsolutePath().equals(xspecFile.getAbsolutePath())) {

			if (xmlDoc != null) {
				addParamToGeneratedTestFile(generatedTestFile, generatedTestFileEncoding);
			}

			currentXspecTestsScenario = new XslScenario(generatedTestFile);
			currentXspecFile = xspecFile;
		}

		if (xmlDoc != null) {
			currentXspecTestsScenario.setParameter("docUrl", xmlDoc.getFile().getAbsolutePath());
		}

		currentXspecTestsScenario.getTransformer().reset();
		currentXspecTestsScenario.setSaveOutputOnDisk(true);
		currentXspecTestsScenario.setMainOutputDir(generatedTestFile.getParentFile());
		currentXspecTestsScenario.setStoreResultsInSubDir(false);
		currentXspecTestsScenario.setSaveRunReport(true);
		currentXspecTestsScenario.setSaveXmlSource(true);
		currentXspecTestsScenario.setMainOutputName(xspecFile.getName().replace(".xspec", "-result.xml"));
		currentXspecTestsScenario.setInitialTemplate("{http://www.jenitennison.com/xslt/xspec}main");
		currentXspecTestsScenario.setName(xspecFile.getName().replaceAll(".xspec", "_xspec"));

		// FIXME xspecFile could be omitted since initialTemplate has been
		// set? Create an apply() method without arg?
		currentXspecTestsScenario.apply(xspecFile);

		return currentXspecTestsScenario.getLastRunReport();
	}

	/**
	 * Generate test XSL
	 * 
	 * @param xspecFile
	 * @return
	 */
	private RunReport generateTestFile(File xspecFile) {

		xspecTestsGeneratorScenario.getTransformer().reset();

		xspecTestsGeneratorScenario.setMainOutputDir(outputDir);

		xspecTestsGeneratorScenario.setStoreResultsInSubDir(storeResultsInSubDir);

		xspecTestsGeneratorScenario.setResultsSubDirWithTimeStamp(resultsSubDirWithTimeStamp);

		xspecTestsGeneratorScenario.setName(xspecFile.getName().replace(".xspec", ""));

		xspecTestsGeneratorScenario.setMainOutputName(xspecFile.getName().replace(".xspec", ".xslt"));

		xspecTestsGeneratorScenario.setSaveOutputOnDisk(true);

		xspecTestsGeneratorScenario.apply(xspecFile);

		return xspecTestsGeneratorScenario.getLastRunReport();
	}

	public XslScenario getXspecTestsGeneratorScenario() {
		return xspecTestsGeneratorScenario;
	}

	public void setXspecTestsGeneratorScenario(XslScenario xspecTestsGeneratorScenario) {
		this.xspecTestsGeneratorScenario = xspecTestsGeneratorScenario;
	}

	public XslScenario getXspecResultHtmlConvertorScenario() {
		return xspecResultHtmlConvertorScenario;
	}

	public void setXspecResultHtmlConvertorScenario(XslScenario xspecResultHtmlConvertorScenario) {
		this.xspecResultHtmlConvertorScenario = xspecResultHtmlConvertorScenario;
	}

	public void cleanOutputDir() {
		try {
			FileUtils.cleanDirectory(outputDir);
		} catch (IOException e) {
			logger.equals(e);
		}

	}

	public boolean isResultsSubDirWithTimeStamp() {
		return resultsSubDirWithTimeStamp;
	}

	public void setResultsSubDirWithTimeStamp(boolean resultsSubDirWithTimeStamp) {
		this.resultsSubDirWithTimeStamp = resultsSubDirWithTimeStamp;
	}

	public boolean isStoreResultsInSubDir() {
		return storeResultsInSubDir;
	}

	public void setStoreResultsInSubDir(boolean storeResultsInSubDir) {
		this.storeResultsInSubDir = storeResultsInSubDir;
	}

	private boolean getSuccess(File mainOutputFile) {
		String success = "false";

		try {
			success = successXpath.evaluate(new StreamSource(mainOutputFile));
		} catch (XPathExpressionException e) {
			logger.error("Error while retrieving success/failure in test report {}", mainOutputFile, e);
		}

		return Boolean.parseBoolean(success);
	}

	// TODO ????
	private FailureReport getFailureReport(File mainOutputFile) {
		FailureReport failureReport = new FailureReport("XSpec Test failed to run.  See error log.");

		return failureReport;
	}

}
